feat: add new endpoints for fetching feature flags and feature subgraphs by federated graph#2729
feat: add new endpoints for fetching feature flags and feature subgraphs by federated graph#2729JivusAyrus wants to merge 14 commits intomainfrom
Conversation
…phs by federated graph
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR refactors feature flags and feature subgraphs out of Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Router-nonroot image scan failed❌ Security vulnerabilities found in image: Please check the security vulnerabilities found in the PR. If you believe this is a false positive, please add the vulnerability to the |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
controlplane/src/core/repositories/FeatureFlagRepository.ts (1)
495-512: Avoid 2N+1 lookups after paginating.This path paginates in SQL, but each returned row still does
byTargetId()andbyId()serially. With the current 50-item cap, one page can still trigger 100+ extra DB queries. Folding the missing fields into the main query, or batch-loading them by ID, would keep the new endpoint’s latency predictable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 495 - 512, In FeatureFlagRepository where featureSubgraphTargets is iterated, avoid N+1 by collecting all needed IDs (all targetId values and all baseSubgraphId values from featureSubgraphTargets) and batch-loading them once (e.g., using subgraphRepo.byIds or a new subgraphRepo.byTargetIds/byIds method) before the loop; then map the returned subgraph rows by id/targetId and populate featureSubgraphs from those maps (or alternatively fold the missing fields into the primary SQL with joins) instead of calling subgraphRepo.byTargetId and subgraphRepo.byId for each item.controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts (1)
65-66: Assert the exact result set here.This setup creates one eligible flag, but
toBeGreaterThanOrEqual(1)plussome(...)still allows duplicates or unrelated extras. Tightening this to the exact returned IDs/names—or adding a second ineligible flag to the fixture—would actually lock down the “only flags in the latest composition” contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts` around lines 65 - 66, The test currently uses a loose assertion (expect(resp.featureFlags.length).toBeGreaterThanOrEqual(1) plus some(...)), so tighten it to assert the exact returned set: either change the length assertion to expect(resp.featureFlags.length).toBe(1) and assert the single entry matches flagName (e.g., resp.featureFlags[0].name === flagName) or replace both assertions with a deterministic comparison of names/ids (e.g., compare resp.featureFlags.map(f=>f.name) to [flagName]); alternatively, add a second ineligible flag to the fixture and assert the response contains only the eligible flag(s) to lock down the “only flags in the latest composition” contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`:
- Around line 65-84: The loop discards the composed schema-version id from
getFeatureFlagSchemaVersionsByBaseSchemaVersion (ffsInLatestValidComposition)
and re-reads the mutable current flag via featureFlagRepo.getFeatureFlagById,
causing drift; update the loop to load the exact composed flag version using the
schema-version identifier present on each ff (e.g., ff.schemaVersionId or
ff.featureFlagSchemaVersionId) and call the repository method that returns that
specific schema-version record (instead of getFeatureFlagById), then push that
version into featureFlags so the response reflects the flags as they were in the
latest composition.
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts`:
- Around line 70-87: The featureSubgraphs mapping in
getFeatureSubgraphsByFederatedGraph (inside the featureSubgraphs:
featureSubgraphs.map(...) block) omits createdUserId, causing inconsistent
payloads vs getFeatureSubgraphs; add createdUserId: fs.createdUserId to the
object returned by the map, and update any related type/interface (feature
subgraph DTO or return type) if necessary so createdUserId is included
throughout; keep other fields (e.g., routingURL, subscriptionProtocol,
websocketSubprotocol, type via convertToSubgraphType) unchanged.
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 437-439: getFeatureSubgraphs currently restricts the query filter
to like(targets.name, `%${query}%`) which breaks "Search by ID or Name"; modify
the query handling so the conditions array includes an OR branch that also
checks subgraphs.id for an exact match when query is a UUID (or always include
equality on subgraphs.id alongside the name like). Locate the code that pushes
like(targets.name, `%${query}%`) and add a complementary condition that checks
subgraphs.id (use equals/subgraphs.id === query or the repository's equality
helper) so searches by feature-subgraph ID continue to return results.
In
`@controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts`:
- Around line 313-323: Replace the loose .some() check with a strict cardinality
assertion: for the call to client.getFeatureSubgraphsByFederatedGraph in this
test, verify that resp.featureSubgraphs contains exactly one entry named
"users-feature" (e.g., count/filter the entries with s.name === 'users-feature'
and expect the count to equal 1) and optionally assert that
resp.featureSubgraphs has no duplicate names by comparing its length to the
length of the set/unique list of names; update the test that references
resp.featureSubgraphs to enforce these exact-unique expectations.
In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 745-763: Extract the feature-flag name resolution into a new
constant (e.g., selectedFeatureFlagName) by reading
compositionFlagsData?.featureFlags and finding the flag with id ===
loadSchemaGraphId, and then modify the useQuery call for
getFederatedGraphSDLByName to include an enabled condition so it only runs when
either type !== 'featureFlag' or (type === 'featureFlag' && compositionFlagsData
has resolved and selectedFeatureFlagName is defined); update the query's
featureFlagName to use selectedFeatureFlagName so the SDL query does not fire
with undefined and avoids the unnecessary re-fetch/flicker.
---
Nitpick comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 495-512: In FeatureFlagRepository where featureSubgraphTargets is
iterated, avoid N+1 by collecting all needed IDs (all targetId values and all
baseSubgraphId values from featureSubgraphTargets) and batch-loading them once
(e.g., using subgraphRepo.byIds or a new subgraphRepo.byTargetIds/byIds method)
before the loop; then map the returned subgraph rows by id/targetId and populate
featureSubgraphs from those maps (or alternatively fold the missing fields into
the primary SQL with joins) instead of calling subgraphRepo.byTargetId and
subgraphRepo.byId for each item.
In
`@controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts`:
- Around line 65-66: The test currently uses a loose assertion
(expect(resp.featureFlags.length).toBeGreaterThanOrEqual(1) plus some(...)), so
tighten it to assert the exact returned set: either change the length assertion
to expect(resp.featureFlags.length).toBe(1) and assert the single entry matches
flagName (e.g., resp.featureFlags[0].name === flagName) or replace both
assertions with a deterministic comparison of names/ids (e.g., compare
resp.featureFlags.map(f=>f.name) to [flagName]); alternatively, add a second
ineligible flag to the fixture and assert the response contains only the
eligible flag(s) to lock down the “only flags in the latest composition”
contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a8ab9dd9-6e5d-4ab3-ab4c-328fa99f892d
⛔ Files ignored due to path filters (2)
connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.gois excluded by!**/*.pb.go,!**/gen/**connect-go/gen/proto/wg/cosmo/platform/v1/platformv1connect/platform.connect.gois excluded by!**/gen/**
📒 Files selected for processing (16)
connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.tsconnect/src/wg/cosmo/platform/v1/platform_connect.tsconnect/src/wg/cosmo/platform/v1/platform_pb.tscontrolplane/src/core/bufservices/PlatformService.tscontrolplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.tscontrolplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.tscontrolplane/src/core/bufservices/federated-graph/getFederatedGraphByName.tscontrolplane/src/core/repositories/FeatureFlagRepository.tscontrolplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.tscontrolplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.tsproto/wg/cosmo/platform/v1/platform.protostudio/src/components/layout/graph-layout.tsxstudio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxstudio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/index.tsxstudio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/sdl.tsxstudio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/subgraphs.tsx
💤 Files with no reviewable changes (2)
- studio/src/components/layout/graph-layout.tsx
- controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
| // Get feature flag IDs from the latest valid composition | ||
| const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsByBaseSchemaVersion({ | ||
| baseSchemaVersionId: federatedGraph.schemaVersionId, | ||
| }); | ||
|
|
||
| const featureFlags: FeatureFlagDTO[] = []; | ||
| if (ffsInLatestValidComposition) { | ||
| for (const ff of ffsInLatestValidComposition) { | ||
| if (!ff.featureFlagId) { | ||
| continue; | ||
| } | ||
| const flag = await featureFlagRepo.getFeatureFlagById({ | ||
| featureFlagId: ff.featureFlagId, | ||
| namespaceId: namespace.id, | ||
| includeSubgraphs: false, | ||
| }); | ||
| if (flag) { | ||
| featureFlags.push(flag); | ||
| } | ||
| } |
There was a problem hiding this comment.
This returns the current flag record, not the flag version in the latest composition.
getFeatureFlagSchemaVersionsByBaseSchemaVersion() already tells us which schema-version rows were composed, but the loop throws that version id away and reloads by mutable featureFlagId. If a flag is edited or deleted after the last valid composition, this endpoint will drift from what was actually composed—or omit the flag entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`
around lines 65 - 84, The loop discards the composed schema-version id from
getFeatureFlagSchemaVersionsByBaseSchemaVersion (ffsInLatestValidComposition)
and re-reads the mutable current flag via featureFlagRepo.getFeatureFlagById,
causing drift; update the loop to load the exact composed flag version using the
schema-version identifier present on each ff (e.g., ff.schemaVersionId or
ff.featureFlagSchemaVersionId) and call the repository method that returns that
specific schema-version record (instead of getFeatureFlagById), then push that
version into featureFlags so the response reflects the flags as they were in the
latest composition.
| featureSubgraphs: featureSubgraphs.map((fs) => ({ | ||
| id: fs.id, | ||
| name: fs.name, | ||
| routingURL: fs.routingUrl, | ||
| lastUpdatedAt: fs.lastUpdatedAt, | ||
| labels: fs.labels, | ||
| targetId: fs.targetId, | ||
| subscriptionUrl: fs.subscriptionUrl, | ||
| namespace: fs.namespace, | ||
| subscriptionProtocol: fs.subscriptionProtocol, | ||
| isEventDrivenGraph: fs.isEventDrivenGraph, | ||
| isV2Graph: fs.isV2Graph, | ||
| websocketSubprotocol: fs.websocketSubprotocol || '', | ||
| isFeatureSubgraph: fs.isFeatureSubgraph, | ||
| baseSubgraphId: fs.baseSubgraphId, | ||
| baseSubgraphName: fs.baseSubgraphName, | ||
| type: convertToSubgraphType(fs.type), | ||
| })), |
There was a problem hiding this comment.
Populate createdUserId in this mapping too.
getFeatureSubgraphs returns the same feature-subgraph payload with createdUserId, but this handler drops it. After the UI switches to this RPC, consumers will get a partially populated featureSubgraph object compared to the existing endpoint.
Proposed fix
featureSubgraphs: featureSubgraphs.map((fs) => ({
id: fs.id,
name: fs.name,
routingURL: fs.routingUrl,
lastUpdatedAt: fs.lastUpdatedAt,
labels: fs.labels,
+ createdUserId: fs.creatorUserId,
targetId: fs.targetId,
subscriptionUrl: fs.subscriptionUrl,
namespace: fs.namespace,
subscriptionProtocol: fs.subscriptionProtocol,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| featureSubgraphs: featureSubgraphs.map((fs) => ({ | |
| id: fs.id, | |
| name: fs.name, | |
| routingURL: fs.routingUrl, | |
| lastUpdatedAt: fs.lastUpdatedAt, | |
| labels: fs.labels, | |
| targetId: fs.targetId, | |
| subscriptionUrl: fs.subscriptionUrl, | |
| namespace: fs.namespace, | |
| subscriptionProtocol: fs.subscriptionProtocol, | |
| isEventDrivenGraph: fs.isEventDrivenGraph, | |
| isV2Graph: fs.isV2Graph, | |
| websocketSubprotocol: fs.websocketSubprotocol || '', | |
| isFeatureSubgraph: fs.isFeatureSubgraph, | |
| baseSubgraphId: fs.baseSubgraphId, | |
| baseSubgraphName: fs.baseSubgraphName, | |
| type: convertToSubgraphType(fs.type), | |
| })), | |
| featureSubgraphs: featureSubgraphs.map((fs) => ({ | |
| id: fs.id, | |
| name: fs.name, | |
| routingURL: fs.routingUrl, | |
| lastUpdatedAt: fs.lastUpdatedAt, | |
| labels: fs.labels, | |
| createdUserId: fs.creatorUserId, | |
| targetId: fs.targetId, | |
| subscriptionUrl: fs.subscriptionUrl, | |
| namespace: fs.namespace, | |
| subscriptionProtocol: fs.subscriptionProtocol, | |
| isEventDrivenGraph: fs.isEventDrivenGraph, | |
| isV2Graph: fs.isV2Graph, | |
| websocketSubprotocol: fs.websocketSubprotocol || '', | |
| isFeatureSubgraph: fs.isFeatureSubgraph, | |
| baseSubgraphId: fs.baseSubgraphId, | |
| baseSubgraphName: fs.baseSubgraphName, | |
| type: convertToSubgraphType(fs.type), | |
| })), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts`
around lines 70 - 87, The featureSubgraphs mapping in
getFeatureSubgraphsByFederatedGraph (inside the featureSubgraphs:
featureSubgraphs.map(...) block) omits createdUserId, causing inconsistent
payloads vs getFeatureSubgraphs; add createdUserId: fs.createdUserId to the
object returned by the map, and update any related type/interface (feature
subgraph DTO or return type) if necessary so createdUserId is included
throughout; keep other fields (e.g., routingURL, subscriptionProtocol,
websocketSubprotocol, type via convertToSubgraphType) unchanged.
controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts
Show resolved
Hide resolved
| const { data: compositionFlagsData } = useQuery( | ||
| getFeatureFlagsInLatestCompositionByFederatedGraph, | ||
| { | ||
| federatedGraphName: graphContext?.graph?.name, | ||
| namespace: graphContext?.graph?.namespace, | ||
| }, | ||
| { | ||
| enabled: !!graphContext?.graph?.name, | ||
| }, | ||
| ); | ||
|
|
||
| const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, { | ||
| name: graphContext?.graph?.name, | ||
| namespace: graphContext?.graph?.namespace, | ||
| featureFlagName: | ||
| type === 'featureFlag' | ||
| ? graphContext?.featureFlagsInLatestValidComposition.find((f) => f.id === loadSchemaGraphId)?.name | ||
| ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name | ||
| : undefined, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "playground.tsx" | grep -E "graph/\[slug\]"Repository: wundergraph/cosmo
Length of output: 138
🏁 Script executed:
cat studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | sed -n '730,770p'Repository: wundergraph/cosmo
Length of output: 1469
🏁 Script executed:
# Get broader context to understand compositionFlagsData and query setup
cat studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | sed -n '735,800p'Repository: wundergraph/cosmo
Length of output: 2329
🏁 Script executed:
# Search for usage of compositionFlagsData and featureFlagName in the file
rg "compositionFlagsData|featureFlagName" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -nRepository: wundergraph/cosmo
Length of output: 767
🏁 Script executed:
# Check the useQuery hook implementation to understand how enabled works
rg "export.*useQuery" studio/src --type ts --type tsx -A 5 | head -50Repository: wundergraph/cosmo
Length of output: 88
🏁 Script executed:
# Look for imports to understand the query library being used
head -50 studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | grep -E "^import|^from"Repository: wundergraph/cosmo
Length of output: 1837
🏁 Script executed:
# Check the subgraphData query to see how it uses enabled condition
sed -n '765,780p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 680
🏁 Script executed:
# Look for all useQuery calls to understand the pattern
rg "useQuery\(" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 1 -A 8Repository: wundergraph/cosmo
Length of output: 1535
🏁 Script executed:
# Check if there are any test files or integration tests related to feature flags
find . -type f \( -name "*.test.tsx" -o -name "*.spec.tsx" \) | xargs grep -l "featureFlag\|compositionFlags" 2>/dev/null | head -5Repository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
# Check the actual behavior of useQuery - look for how it handles missing enabled condition
# and understand if queries fire immediately
rg "enabled:" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 1Repository: wundergraph/cosmo
Length of output: 322
🏁 Script executed:
# Look at the downstream usage of featureFlagName in the fetcher/request headers
sed -n '185,230p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 1271
🏁 Script executed:
# Check if there's any dependency array or effect logic that might prevent the race condition
sed -n '800,850p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 1523
🏁 Script executed:
# Look at where graphiQLFetch is called to understand how featureFlagName flows through
rg "graphiQLFetch\(" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 10Repository: wundergraph/cosmo
Length of output: 527
🏁 Script executed:
# Check if there's any caching mechanism that might prevent the double-fetch issue
rg "createGraphiQLFetcher" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 5 -A 20Repository: wundergraph/cosmo
Length of output: 2178
🏁 Script executed:
# Understand the exact moment when compositionFlagsData is available
# Check if there's any dependency on type or loadSchemaGraphId in the composition flags query
sed -n '745,754p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 344
🏁 Script executed:
# Check the fetcher dependency array to see if it includes compositionFlagsData
rg "createGraphiQLFetcher" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -A 25 | grep -A 20 "^\]"Repository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
# Verify the exact location of the second feature flag lookup mentioned in the comment
sed -n '1005,1025p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 662
🏁 Script executed:
# Check the structure of useQuery to confirm it supports the three-argument pattern with options
rg "function useQuery|export.*useQuery" studio/src -A 15 | head -80Repository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
# Verify if there are other similar patterns where enabled gate is used for dependent queries
rg "enabled:.*\|\|.*\?" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
# Check how the createGraphiQLFetcher is used and if schema changes when compositionFlagsData arrives
sed -n '980,1030p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 1538
🏁 Script executed:
# Check the connect-query version and documentation reference
rg "@connectrpc/connect-query" studio/package.jsonRepository: wundergraph/cosmo
Length of output: 103
🏁 Script executed:
# Look at how compositionFlagsData is used and when it's expected to be available
sed -n '745,800p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 1939
🏁 Script executed:
# Verify the exact scenario: does the SDL query have compositionFlagsData in its dependency tracking?
rg "getFederatedGraphSDLByName" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 12Repository: wundergraph/cosmo
Length of output: 1369
🏁 Script executed:
# Check if there's any query key hashing or dependency tracking in connect-query
# Look at how parameters affect query caching
web_search "connectrpc connect-query useQuery enabled option documentation"Repository: wundergraph/cosmo
Length of output: 111
🏁 Script executed:
# Look more carefully at when the schema variable (used in fetcher) updates
# Check the useMemo dependency for schema
sed -n '775,790p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 666
🏁 Script executed:
# Verify the complete pattern of how data flows from SDL query to fetcher to GraphiQL
sed -n '1000,1015p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 569
🏁 Script executed:
# Let's verify the actual runtime behavior by checking if there are any tests or examples
# that show this scenario
rg "type === 'featureFlag'" studio/src -B 3 -A 3Repository: wundergraph/cosmo
Length of output: 4208
🏁 Script executed:
# Check if there's any stale-while-revalidate or background revalidation that might mask the issue
rg "staleTime|cacheTime|gcTime" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
# Get the complete useQuery call for SDL to verify there's no enabled option present
sed -n '756,764p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsxRepository: wundergraph/cosmo
Length of output: 412
Wait for the selected feature flag before loading SDL.
When type === 'featureFlag', the SDL query derives featureFlagName from compositionFlagsData, but that query loads independently and completes asynchronously. Without gating, getFederatedGraphSDLByName fires immediately on first render with featureFlagName: undefined, hydrating GraphiQL with the base graph schema. Once compositionFlagsData resolves and the flag name becomes available, the query key changes and fires again, causing an unnecessary re-fetch and schema flicker. Extract the flag name lookup into selectedFeatureFlagName and add an enabled condition to prevent the SDL query from running until the flag is resolved.
💡 Proposed fix
+ const selectedFeatureFlagName =
+ type === 'featureFlag'
+ ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
+ : undefined;
+
- const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, {
- name: graphContext?.graph?.name,
- namespace: graphContext?.graph?.namespace,
- featureFlagName:
- type === 'featureFlag'
- ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
- : undefined,
- });
+ const { data, isLoading: isLoadingGraphSchema } = useQuery(
+ getFederatedGraphSDLByName,
+ {
+ name: graphContext?.graph?.name,
+ namespace: graphContext?.graph?.namespace,
+ featureFlagName: selectedFeatureFlagName,
+ },
+ {
+ enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName),
+ },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: compositionFlagsData } = useQuery( | |
| getFeatureFlagsInLatestCompositionByFederatedGraph, | |
| { | |
| federatedGraphName: graphContext?.graph?.name, | |
| namespace: graphContext?.graph?.namespace, | |
| }, | |
| { | |
| enabled: !!graphContext?.graph?.name, | |
| }, | |
| ); | |
| const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, { | |
| name: graphContext?.graph?.name, | |
| namespace: graphContext?.graph?.namespace, | |
| featureFlagName: | |
| type === 'featureFlag' | |
| ? graphContext?.featureFlagsInLatestValidComposition.find((f) => f.id === loadSchemaGraphId)?.name | |
| ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name | |
| : undefined, | |
| }); | |
| const { data: compositionFlagsData } = useQuery( | |
| getFeatureFlagsInLatestCompositionByFederatedGraph, | |
| { | |
| federatedGraphName: graphContext?.graph?.name, | |
| namespace: graphContext?.graph?.namespace, | |
| }, | |
| { | |
| enabled: !!graphContext?.graph?.name, | |
| }, | |
| ); | |
| const selectedFeatureFlagName = | |
| type === 'featureFlag' | |
| ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name | |
| : undefined; | |
| const { data, isLoading: isLoadingGraphSchema } = useQuery( | |
| getFederatedGraphSDLByName, | |
| { | |
| name: graphContext?.graph?.name, | |
| namespace: graphContext?.graph?.namespace, | |
| featureFlagName: selectedFeatureFlagName, | |
| }, | |
| { | |
| enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName), | |
| }, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
around lines 745 - 763, Extract the feature-flag name resolution into a new
constant (e.g., selectedFeatureFlagName) by reading
compositionFlagsData?.featureFlags and finding the flag with id ===
loadSchemaGraphId, and then modify the useQuery call for
getFederatedGraphSDLByName to include an enabled condition so it only runs when
either type !== 'featureFlag' or (type === 'featureFlag' && compositionFlagsData
has resolved and selectedFeatureFlagName is defined); update the query's
featureFlagName to use selectedFeatureFlagName so the SDL query does not fire
with undefined and avoids the unnecessary re-fetch/flicker.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx (1)
745-763:⚠️ Potential issue | 🟠 MajorPrevent feature-flag mode from loading SDL before flag resolution.
Line 756 still executes the SDL query before the selected flag name exists (Line 761), so feature-flag mode can fetch base schema first and re-fetch after flags load. Also, the flag lookup is duplicated in three places (Line 761, Line 1012, Line 1067), which risks drift.
💡 Proposed fix
const { data: compositionFlagsData } = useQuery( getFeatureFlagsInLatestCompositionByFederatedGraph, @@ }, ); + const selectedFeatureFlagName = + type === 'featureFlag' + ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name + : undefined; - const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, { - name: graphContext?.graph?.name, - namespace: graphContext?.graph?.namespace, - featureFlagName: - type === 'featureFlag' - ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name - : undefined, - }); + const { data, isLoading: isLoadingGraphSchema } = useQuery( + getFederatedGraphSDLByName, + { + name: graphContext?.graph?.name, + namespace: graphContext?.graph?.namespace, + featureFlagName: selectedFeatureFlagName, + }, + { + enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName), + }, + ); @@ graphContext?.graph?.id || '', - type === 'featureFlag' - ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name - : undefined, + selectedFeatureFlagName, ), @@ - if (type === 'featureFlag') { - const featureFlag = (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId); - if (featureFlag) { - requestHeaders['X-Feature-Flag'] = featureFlag.name; - } + if (selectedFeatureFlagName) { + requestHeaders['X-Feature-Flag'] = selectedFeatureFlagName; } @@ - compositionFlagsData?.featureFlags, + selectedFeatureFlagName, @@ - compositionFlagsData?.featureFlags, + selectedFeatureFlagName,Also applies to: 1011-1013, 1066-1070
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx around lines 745 - 763, Prevent the SDL query from running in feature-flag mode until the flags are loaded and the selected flag is resolved: compute a single selectedFlagName variable by deriving it once from compositionFlagsData, loadSchemaGraphId and use that name wherever needed (replace the repeated .find usages at the three spots), and pass featureFlagName: selectedFlagName to the getFederatedGraphSDLByName useQuery; also set the useQuery enabled flag to only run when either type !== 'featureFlag' or selectedFlagName is truthy so the base SDL is not fetched prematurely.controlplane/src/core/repositories/FeatureFlagRepository.ts (1)
437-439:⚠️ Potential issue | 🟡 MinorKeep feature-subgraph ID search working.
This new path only filters on
targets.name, so UUID searches that already work ingetFeatureSubgraphs()stop returning results here.💡 Proposed fix
if (query) { - conditions.push(like(targets.name, `%${query}%`)); + conditions.push(isValidUuid(query) ? eq(subgraphs.id, query) : like(targets.name, `%${query}%`)); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 437 - 439, The current query filter only matches targets.name so UUID searches stop returning results; in the method that builds conditions (where you have if (query) { conditions.push(like(targets.name, `%${query}%`)); }) update it to also check the feature-subgraph id—e.g., add a second condition on targets.id (exact match) or OR a like on targets.id when query looks like a UUID—so getFeatureSubgraphs()/the same query path will return results for ID searches as well; modify the conditions.push call(s) to include targets.id alongside targets.name (and optionally detect UUID format to prefer exact match).controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts (1)
69-83:⚠️ Potential issue | 🟠 MajorThis returns current flag state, not the latest composed snapshot.
getFeatureFlagSchemaVersionsByBaseSchemaVersion()gives you the persisted composition membership, but this loop throws that record away and reloads the mutable live flag byfeatureFlagId. If a flag is disabled, relabeled, or deleted after that composition, this RPC will drift from what the latest composition actually contained.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts` around lines 69 - 83, The code currently discards the composition membership returned by getFeatureFlagSchemaVersionsByBaseSchemaVersion (ffsInLatestValidComposition) and reloads live flags via featureFlagRepo.getFeatureFlagById, which returns the current mutable state instead of the snapshot that was part of the composition; change the lookup to fetch the versioned/snapshot data tied to the composition record (e.g., use the schema-version-scoped accessor instead of getFeatureFlagById) — for example call a repository method that returns the feature-flag snapshot by featureFlagId + base/schema version (or use the data already present on the ff record) so you populate featureFlags from the composed snapshot (reference symbols: getFeatureFlagSchemaVersionsByBaseSchemaVersion, ffsInLatestValidComposition, ff.featureFlagId, featureFlagRepo.getFeatureFlagById, featureFlags).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 467-484: The ORDER BY in FeatureFlagRepository's query uses only
targets.createdAt (baseQuery.orderBy(asc(targets.createdAt))) which is
non-deterministic for identical timestamps; update the ordering to include a
stable tiebreaker (e.g., secondary sort by targets.id or
subgraphsToFederatedGraph.id) so pagination with limit/offset is deterministic,
and keep the count query as-is; locate where baseQuery.orderBy(...) is called
and add the secondary asc(...) tiebreaker to the same orderBy chain.
In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 619-629: The query for composition flags is being enabled only by
graphContext?.graph?.name but reads router.query.namespace into the namespace
const without guarding for its availability, which can fire a request with a
missing namespace during router hydration; update how namespace is derived
(allow undefined) and change the useQuery enabled option for
getFeatureFlagsInLatestCompositionByFederatedGraph to require both
graphContext?.graph?.name and namespace (e.g., enabled:
!!graphContext?.graph?.name && !!namespace) so the query only runs when
namespace is present.
---
Duplicate comments:
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`:
- Around line 69-83: The code currently discards the composition membership
returned by getFeatureFlagSchemaVersionsByBaseSchemaVersion
(ffsInLatestValidComposition) and reloads live flags via
featureFlagRepo.getFeatureFlagById, which returns the current mutable state
instead of the snapshot that was part of the composition; change the lookup to
fetch the versioned/snapshot data tied to the composition record (e.g., use the
schema-version-scoped accessor instead of getFeatureFlagById) — for example call
a repository method that returns the feature-flag snapshot by featureFlagId +
base/schema version (or use the data already present on the ff record) so you
populate featureFlags from the composed snapshot (reference symbols:
getFeatureFlagSchemaVersionsByBaseSchemaVersion, ffsInLatestValidComposition,
ff.featureFlagId, featureFlagRepo.getFeatureFlagById, featureFlags).
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 437-439: The current query filter only matches targets.name so
UUID searches stop returning results; in the method that builds conditions
(where you have if (query) { conditions.push(like(targets.name, `%${query}%`));
}) update it to also check the feature-subgraph id—e.g., add a second condition
on targets.id (exact match) or OR a like on targets.id when query looks like a
UUID—so getFeatureSubgraphs()/the same query path will return results for ID
searches as well; modify the conditions.push call(s) to include targets.id
alongside targets.name (and optionally detect UUID format to prefer exact
match).
In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 745-763: Prevent the SDL query from running in feature-flag mode
until the flags are loaded and the selected flag is resolved: compute a single
selectedFlagName variable by deriving it once from compositionFlagsData,
loadSchemaGraphId and use that name wherever needed (replace the repeated .find
usages at the three spots), and pass featureFlagName: selectedFlagName to the
getFederatedGraphSDLByName useQuery; also set the useQuery enabled flag to only
run when either type !== 'featureFlag' or selectedFlagName is truthy so the base
SDL is not fetched prematurely.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 244afbfd-1f02-4c3d-bc07-467895044470
📒 Files selected for processing (5)
controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.tscontrolplane/src/core/repositories/FeatureFlagRepository.tscontrolplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.tscontrolplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.tsstudio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
✅ Files skipped from review due to trivial changes (2)
- controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts
- controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts
| .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId))) | ||
| .orderBy(asc(targets.createdAt)); | ||
|
|
||
| // Get total count | ||
| const countResult = await this.db | ||
| .select({ count: count() }) | ||
| .from(baseQuery.as('feature_subgraphs_by_fed_graph')) | ||
| .execute(); | ||
|
|
||
| const totalCount = countResult[0]?.count ?? 0; | ||
|
|
||
| // Apply pagination | ||
| if (limit > 0) { | ||
| baseQuery.limit(limit); | ||
| } | ||
| if (offset > 0) { | ||
| baseQuery.offset(offset); | ||
| } |
There was a problem hiding this comment.
Add a stable tiebreaker to the paginated ordering.
ORDER BY targets.createdAt alone is not deterministic when multiple rows share the same timestamp, so offset-based paging can duplicate or skip feature subgraphs between pages.
💡 Proposed fix
.innerJoin(namespaces, eq(namespaces.id, targets.namespaceId))
.where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
- .orderBy(asc(targets.createdAt));
+ .orderBy(asc(targets.createdAt), asc(subgraphs.id));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId))) | |
| .orderBy(asc(targets.createdAt)); | |
| // Get total count | |
| const countResult = await this.db | |
| .select({ count: count() }) | |
| .from(baseQuery.as('feature_subgraphs_by_fed_graph')) | |
| .execute(); | |
| const totalCount = countResult[0]?.count ?? 0; | |
| // Apply pagination | |
| if (limit > 0) { | |
| baseQuery.limit(limit); | |
| } | |
| if (offset > 0) { | |
| baseQuery.offset(offset); | |
| } | |
| .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId))) | |
| .orderBy(asc(targets.createdAt), asc(subgraphs.id)); | |
| // Get total count | |
| const countResult = await this.db | |
| .select({ count: count() }) | |
| .from(baseQuery.as('feature_subgraphs_by_fed_graph')) | |
| .execute(); | |
| const totalCount = countResult[0]?.count ?? 0; | |
| // Apply pagination | |
| if (limit > 0) { | |
| baseQuery.limit(limit); | |
| } | |
| if (offset > 0) { | |
| baseQuery.offset(offset); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 467
- 484, The ORDER BY in FeatureFlagRepository's query uses only targets.createdAt
(baseQuery.orderBy(asc(targets.createdAt))) which is non-deterministic for
identical timestamps; update the ordering to include a stable tiebreaker (e.g.,
secondary sort by targets.id or subgraphsToFederatedGraph.id) so pagination with
limit/offset is deterministic, and keep the count query as-is; locate where
baseQuery.orderBy(...) is called and add the secondary asc(...) tiebreaker to
the same orderBy chain.
| const namespace = router.query.namespace as string; | ||
|
|
||
| const { data: compositionFlagsData } = useQuery( | ||
| getFeatureFlagsInLatestCompositionByFederatedGraph, | ||
| { | ||
| federatedGraphName: graphContext?.graph?.name, | ||
| namespace, | ||
| }, | ||
| { | ||
| enabled: !!graphContext?.graph?.name, | ||
| }, |
There was a problem hiding this comment.
Guard the composition-flags query on namespace availability.
Line 619 reads router.query.namespace directly, but Line 628 enables the query only by graph name. During initial router hydration this can issue a request with missing namespace.
💡 Proposed fix
- const namespace = router.query.namespace as string;
+ const namespace =
+ (router.query.namespace as string | undefined) ?? graphContext?.graph?.namespace ?? '';
@@
{
- enabled: !!graphContext?.graph?.name,
+ enabled: !!graphContext?.graph?.name && !!namespace,
},
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
around lines 619 - 629, The query for composition flags is being enabled only by
graphContext?.graph?.name but reads router.query.namespace into the namespace
const without guarding for its availability, which can fire a request with a
missing namespace during router hydration; update how namespace is derived
(allow undefined) and change the useQuery enabled option for
getFeatureFlagsInLatestCompositionByFederatedGraph to require both
graphContext?.graph?.name and namespace (e.g., enabled:
!!graphContext?.graph?.name && !!namespace) so the query only runs when
namespace is present.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
controlplane/src/core/repositories/FederatedGraphRepository.ts (1)
487-490:⚠️ Potential issue | 🟠 MajorUse
schema.targets.namespaceIdin the mixed RBAC branch.Line 489 compares namespace IDs against
schema.targets.id, which will not match. Actors with both namespace-scoped and resource-scoped graph access lose the namespace portion of their permissions, causinglist()to under-return graphs they should access.Suggested change
conditions.push( - or(inArray(schema.targets.id, [...new Set(namespaces)]), inArray(schema.targets.id, [...new Set(resources)])), + or( + inArray(schema.targets.namespaceId, [...new Set(namespaces)]), + inArray(schema.targets.id, [...new Set(resources)]), + ), );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FederatedGraphRepository.ts` around lines 487 - 490, In FederatedGraphRepository (the mixed RBAC branch inside the list/filter construction), change the left-hand field used for namespace checks from schema.targets.id to schema.targets.namespaceId so namespace-scoped permissions are matched correctly; specifically, update the or(...) call that currently does or(inArray(schema.targets.id, [...new Set(namespaces)]), inArray(schema.targets.id, [...new Set(resources)])) to use schema.targets.namespaceId for the namespaces side while keeping schema.targets.id for resource-scoped checks and the existing de-duplication with new Set.
♻️ Duplicate comments (1)
controlplane/src/core/repositories/FeatureFlagRepository.ts (1)
467-468: 🛠️ Refactor suggestion | 🟠 MajorAdd a stable tiebreaker to the paginated ordering.
ORDER BY targets.createdAtalone is not deterministic when multiple rows share the same timestamp, sooffset-based paging can duplicate or skip feature subgraphs between pages.💡 Proposed fix
.innerJoin(namespaces, eq(namespaces.id, targets.namespaceId)) .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId))) - .orderBy(asc(targets.createdAt)); + .orderBy(asc(targets.createdAt), asc(subgraphs.id));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 467 - 468, The ORDER BY in FeatureFlagRepository using .orderBy(asc(targets.createdAt)) is not deterministic and can cause duplicates/skips during offset paging; update the query that builds the ordering (the call chaining ending with .orderBy(...)) to add a stable tiebreaker such as the primary key (e.g., targets.id or targets.pk) and include it with the same sort direction (e.g., .orderBy(asc(targets.createdAt), asc(targets.id))) so rows with identical createdAt values are consistently ordered.
🧹 Nitpick comments (3)
controlplane/src/core/repositories/FeatureFlagRepository.ts (2)
251-253: Redundant condition can be simplified.Since
isOrganizationVieweris defined asisOrganizationAdminOrDeveloper || this.roles.includes('organization-viewer')(perRBACEvaluator.ts:80), checkingisOrganizationAdminOrDeveloperseparately is redundant.💡 Proposed simplification
private applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions: (SQL<unknown> | undefined)[]) { - if (!rbac || rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer) { + if (!rbac || rbac.isOrganizationViewer) { return true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 251 - 253, The condition in FeatureFlagRepository (variable rbac) redundantly checks rbac.isOrganizationAdminOrDeveloper and rbac.isOrganizationViewer; since RBACEvaluator's isOrganizationViewer already includes isOrganizationAdminOrDeveloper, simplify the guard to only check for absence of rbac or rbac.isOrganizationViewer (i.e., replace the current if (!rbac || rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer) return true; with if (!rbac || rbac.isOrganizationViewer) return true;), referencing the RBACEvaluator properties to justify the change.
489-505: N+1 query pattern could be optimized with batch fetching.Each iteration executes two separate DB queries (
byTargetIdandbyId). With a page of N results, this generates 2N additional queries. Consider collecting alltargetIds andbaseSubgraphIds upfront and fetching them in bulk.💡 Proposed batch approach
const featureSubgraphTargets = await baseQuery.execute(); + // Batch fetch all required data + const targetIds = featureSubgraphTargets.map((f) => f.targetId); + const baseSubgraphIds = [...new Set(featureSubgraphTargets.map((f) => f.baseSubgraphId))]; + + const [subgraphsByTarget, baseSubgraphsById] = await Promise.all([ + subgraphRepo.byTargetIds(targetIds), // Would need to add this batch method + subgraphRepo.byIds(baseSubgraphIds), // Would need to add this batch method + ]); + + const targetMap = new Map(subgraphsByTarget.map((s) => [s.targetId, s])); + const baseMap = new Map(baseSubgraphsById.map((s) => [s.id, s])); + const featureSubgraphs: FeatureSubgraphDTO[] = []; for (const f of featureSubgraphTargets) { - const fs = await subgraphRepo.byTargetId(f.targetId); + const fs = targetMap.get(f.targetId); if (!fs) { continue; } - const baseSubgraph = await subgraphRepo.byId(f.baseSubgraphId); + const baseSubgraph = baseMap.get(f.baseSubgraphId); if (!baseSubgraph) { continue; }Note: This would require adding batch methods to
SubgraphRepositoryif they don't exist.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 489 - 505, The loop over featureSubgraphTargets causes an N+1 query pattern by calling subgraphRepo.byTargetId and subgraphRepo.byId per item; collect all targetIds and baseSubgraphIds from featureSubgraphTargets first, add or use batch methods on SubgraphRepository (e.g., byTargetIds(targetIds[]) and byIds(baseIds[])) to fetch maps of Subgraph objects in two bulk queries, then iterate featureSubgraphTargets and look up fs and baseSubgraph from those maps when building featureSubgraphs (replace the per-item calls to byTargetId and byId).controlplane/src/core/repositories/SubgraphRepository.ts (1)
685-685: Add an explicit return type to the new static helper.Callers branch on this value as a boolean, so
: booleanshould be part of the signature instead of being inferred.Suggested change
- static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions: (SQL<unknown> | undefined)[]) { + static applyRbacConditionsToQuery( + rbac: RBACEvaluator | undefined, + conditions: (SQL<unknown> | undefined)[], + ): boolean {As per coding guidelines,
**/*.{ts,tsx}: Use explicit type annotations for function parameters and return types in TypeScript.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controlplane/src/core/repositories/SubgraphRepository.ts` at line 685, The static helper applyRbacConditionsToQuery currently relies on an inferred return type but callers branch on it as a boolean—update its signature to explicitly declare a boolean return type by adding ": boolean" to the method declaration (static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions: (SQL<unknown> | undefined)[]): boolean) and ensure the implementation returns a boolean value in all code paths to satisfy the explicit annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts`:
- Around line 55-61: Remove the two raw console.log separator calls around the
subgraphRepo.listByFederatedGraph call in getFederatedGraphByName; either delete
them or replace with a single structured logger.debug (using the module's
logger) if you need trace info, and keep the call to
subgraphRepo.listByFederatedGraph({ federatedGraphTargetId:
federatedGraph.targetId, published: false, rbac: authContext.rbac }) unchanged.
In `@controlplane/src/core/repositories/SubgraphRepository.ts`:
- Around line 985-1023: The mapper in listByFederatedGraph is creating a
ProtoSubgraph for every gRPC subgraph even when schemaVersionId is missing and
it fails to populate plugin metadata for grpc_plugin; update the mapping so that
proto is only created when sg.schemaVersionId (or svSchemaSDL equivalent) exists
(matching getSubgraph/byTargetId behavior) and, when sg.type === 'grpc_plugin',
populate proto.pluginData with the plugin image/metadata fields (the same
properties getSubgraph used previously — e.g., plugin image/name/version fields
from the sg record) so the SubgraphDTO/ProtoSubgraph contract is preserved
between listByFederatedGraph and getSubgraph.
- Around line 975-981: The join on schema.protobufSchemaVersions currently
switches between innerJoin and leftJoin based on data.published which causes
non-gRPC subgraphs to be dropped when published is true; change the dynamic join
to always use leftJoin for the protobufSchemaVersions join (the call that
currently uses [data.published ? 'innerJoin' : 'leftJoin'] with
schema.protobufSchemaVersions and the ON clause containing
inArray(schema.subgraphs.type, ['grpc_plugin','grpc_service']) and
eq(schema.subgraphs.schemaVersionId,
schema.protobufSchemaVersions.schemaVersionId)), and make the same change in the
analogous joins in FederatedGraphRepository (the recomposition logic) and
createFederatedGraph so protobuf fields remain nullable and non-gRPC subgraphs
are preserved.
---
Outside diff comments:
In `@controlplane/src/core/repositories/FederatedGraphRepository.ts`:
- Around line 487-490: In FederatedGraphRepository (the mixed RBAC branch inside
the list/filter construction), change the left-hand field used for namespace
checks from schema.targets.id to schema.targets.namespaceId so namespace-scoped
permissions are matched correctly; specifically, update the or(...) call that
currently does or(inArray(schema.targets.id, [...new Set(namespaces)]),
inArray(schema.targets.id, [...new Set(resources)])) to use
schema.targets.namespaceId for the namespaces side while keeping
schema.targets.id for resource-scoped checks and the existing de-duplication
with new Set.
---
Duplicate comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 467-468: The ORDER BY in FeatureFlagRepository using
.orderBy(asc(targets.createdAt)) is not deterministic and can cause
duplicates/skips during offset paging; update the query that builds the ordering
(the call chaining ending with .orderBy(...)) to add a stable tiebreaker such as
the primary key (e.g., targets.id or targets.pk) and include it with the same
sort direction (e.g., .orderBy(asc(targets.createdAt), asc(targets.id))) so rows
with identical createdAt values are consistently ordered.
---
Nitpick comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 251-253: The condition in FeatureFlagRepository (variable rbac)
redundantly checks rbac.isOrganizationAdminOrDeveloper and
rbac.isOrganizationViewer; since RBACEvaluator's isOrganizationViewer already
includes isOrganizationAdminOrDeveloper, simplify the guard to only check for
absence of rbac or rbac.isOrganizationViewer (i.e., replace the current if
(!rbac || rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer)
return true; with if (!rbac || rbac.isOrganizationViewer) return true;),
referencing the RBACEvaluator properties to justify the change.
- Around line 489-505: The loop over featureSubgraphTargets causes an N+1 query
pattern by calling subgraphRepo.byTargetId and subgraphRepo.byId per item;
collect all targetIds and baseSubgraphIds from featureSubgraphTargets first, add
or use batch methods on SubgraphRepository (e.g., byTargetIds(targetIds[]) and
byIds(baseIds[])) to fetch maps of Subgraph objects in two bulk queries, then
iterate featureSubgraphTargets and look up fs and baseSubgraph from those maps
when building featureSubgraphs (replace the per-item calls to byTargetId and
byId).
In `@controlplane/src/core/repositories/SubgraphRepository.ts`:
- Line 685: The static helper applyRbacConditionsToQuery currently relies on an
inferred return type but callers branch on it as a boolean—update its signature
to explicitly declare a boolean return type by adding ": boolean" to the method
declaration (static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined,
conditions: (SQL<unknown> | undefined)[]): boolean) and ensure the
implementation returns a boolean value in all code paths to satisfy the explicit
annotation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 45080fc1-6313-4c83-86f4-56a00831d643
📒 Files selected for processing (6)
controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.tscontrolplane/src/core/bufservices/workspace/getWorkspace.tscontrolplane/src/core/repositories/FeatureFlagRepository.tscontrolplane/src/core/repositories/FederatedGraphRepository.tscontrolplane/src/core/repositories/SubgraphRepository.tscontrolplane/src/core/services/WorkspaceService.ts
controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
Outdated
Show resolved
Hide resolved
| [data.published ? 'innerJoin' : 'leftJoin']( | ||
| schema.protobufSchemaVersions, | ||
| and( | ||
| inArray(schema.subgraphs.type, ['grpc_plugin', 'grpc_service']), | ||
| eq(schema.subgraphs.schemaVersionId, schema.protobufSchemaVersions.schemaVersionId), | ||
| ), | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "SubgraphRepository.ts" | head -20Repository: wundergraph/cosmo
Length of output: 119
🏁 Script executed:
find . -type f -name "FederatedGraphRepository.ts" | head -20Repository: wundergraph/cosmo
Length of output: 125
🏁 Script executed:
find . -type f -name "createFederatedGraph.ts" | head -20Repository: wundergraph/cosmo
Length of output: 136
🏁 Script executed:
wc -l ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 124
🏁 Script executed:
sed -n '960,990p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1465
🏁 Script executed:
sed -n '930,965p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1579
🏁 Script executed:
sed -n '900,935p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1088
🏁 Script executed:
grep -n "listByFederatedGraph" ./controlplane/src/core/repositories/FederatedGraphRepository.tsRepository: wundergraph/cosmo
Length of output: 204
🏁 Script executed:
grep -n "listByFederatedGraph" ./controlplane/src/core/bufservices/federated-graph/createFederatedGraph.tsRepository: wundergraph/cosmo
Length of output: 128
🏁 Script executed:
sed -n '1440,1450p' ./controlplane/src/core/repositories/FederatedGraphRepository.tsRepository: wundergraph/cosmo
Length of output: 501
🏁 Script executed:
sed -n '1544,1554p' ./controlplane/src/core/repositories/FederatedGraphRepository.tsRepository: wundergraph/cosmo
Length of output: 603
🏁 Script executed:
sed -n '194,204p' ./controlplane/src/core/bufservices/federated-graph/createFederatedGraph.tsRepository: wundergraph/cosmo
Length of output: 444
🏁 Script executed:
sed -n '982,1020p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1520
🏁 Script executed:
sed -n '1020,1045p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1075
Always use leftJoin for the protobufSchemaVersions join to prevent silently dropping non-gRPC subgraphs.
When published is true, this becomes an innerJoin whose ON clause filters for only grpc_plugin and grpc_service types. Non-gRPC subgraphs won't match this condition and are silently excluded from the result set. This occurs in both FederatedGraphRepository.ts (line 1547) and createFederatedGraph.ts (line 197) during graph recomposition, potentially causing regular GraphQL subgraphs to disappear from the composed graph without warning. The codebase already handles NULL proto fields correctly (lines 987-997), so using leftJoin consistently is safe and will preserve all subgraphs.
Suggested change
- [data.published ? 'innerJoin' : 'leftJoin'](
+ .leftJoin(
schema.protobufSchemaVersions,
and(
inArray(schema.subgraphs.type, ['grpc_plugin', 'grpc_service']),
eq(schema.subgraphs.schemaVersionId, schema.protobufSchemaVersions.schemaVersionId),
),
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@controlplane/src/core/repositories/SubgraphRepository.ts` around lines 975 -
981, The join on schema.protobufSchemaVersions currently switches between
innerJoin and leftJoin based on data.published which causes non-gRPC subgraphs
to be dropped when published is true; change the dynamic join to always use
leftJoin for the protobufSchemaVersions join (the call that currently uses
[data.published ? 'innerJoin' : 'leftJoin'] with schema.protobufSchemaVersions
and the ON clause containing inArray(schema.subgraphs.type,
['grpc_plugin','grpc_service']) and eq(schema.subgraphs.schemaVersionId,
schema.protobufSchemaVersions.schemaVersionId)), and make the same change in the
analogous joins in FederatedGraphRepository (the recomposition logic) and
createFederatedGraph so protobuf fields remain nullable and non-gRPC subgraphs
are preserved.
| // Transform the selected subgraphs into SubgraphDTO objects | ||
| return subgraphs.map((sg) => { | ||
| let proto: ProtoSubgraph | undefined; | ||
| if (sg.type === 'grpc_plugin' || sg.type === 'grpc_service') { | ||
| if (!sg.protoSchemaVersion) { | ||
| this.logger.warn( | ||
| `Missing protobuf schema for ${sg.type} subgraph with schemaVersionId: ${sg.schemaVersionId}`, | ||
| ); | ||
| } | ||
|
|
||
| for (const target of targets) { | ||
| const sg = await this.byTargetId(target.id); | ||
| if (sg === undefined) { | ||
| continue; | ||
| proto = { | ||
| schema: sg.protoSchemaVersion ?? '', | ||
| mappings: sg.protoMappings ?? '', | ||
| lock: sg.protoLock ?? '', | ||
| }; | ||
| } | ||
| subgraphs.push(sg); | ||
| } | ||
|
|
||
| return subgraphs; | ||
| return { | ||
| id: sg.id, | ||
| targetId: sg.targetId, | ||
| routingUrl: sg.routingUrl, | ||
| readme: sg.readme || undefined, | ||
| subscriptionUrl: sg.subscriptionUrl || '', | ||
| subscriptionProtocol: sg.subscriptionProtocol ?? 'ws', | ||
| websocketSubprotocol: sg.websocketSubprotocol || undefined, | ||
| name: sg.name, | ||
| schemaSDL: sg.svSchemaSDL ?? '', | ||
| schemaVersionId: sg.schemaVersionId || '', | ||
| lastUpdatedAt: sg.svLastUpdated?.toISOString() ?? '', | ||
| labels: sg.labels?.map?.((l) => splitLabel(l)) ?? [], | ||
| creatorUserId: sg.createdBy || undefined, | ||
| namespace: sg.namespaceName, | ||
| namespaceId: sg.namespaceId, | ||
| isEventDrivenGraph: sg.isEventDrivenGraph, | ||
| isV2Graph: sg.svIsV2Graph || undefined, | ||
| isFeatureSubgraph: sg.isFeatureSubgraph, | ||
| type: sg.type, | ||
| proto, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SubgraphRepository.ts" -type fRepository: wundergraph/cosmo
Length of output: 119
🏁 Script executed:
wc -l ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 124
🏁 Script executed:
sed -n '980,1030p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1827
🏁 Script executed:
grep -n "byTargetId" ./controlplane/src/core/repositories/SubgraphRepository.ts | head -20Repository: wundergraph/cosmo
Length of output: 455
🏁 Script executed:
sed -n '1135,1200p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1960
🏁 Script executed:
sed -n '1025,1050p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 1166
🏁 Script executed:
sed -n '1025,1130p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 4117
🏁 Script executed:
sed -n '930,985p' ./controlplane/src/core/repositories/SubgraphRepository.tsRepository: wundergraph/cosmo
Length of output: 2548
🏁 Script executed:
rg -n "ProtoSubgraph" ./controlplane/src/core/repositories/SubgraphRepository.ts | head -10Repository: wundergraph/cosmo
Length of output: 239
🏁 Script executed:
sed -n '1,100p' ./controlplane/src/core/repositories/SubgraphRepository.ts | head -80Repository: wundergraph/cosmo
Length of output: 2274
🏁 Script executed:
rg -n "interface ProtoSubgraph\|type ProtoSubgraph" . --type tsRepository: wundergraph/cosmo
Length of output: 43
🏁 Script executed:
find . -name "*.ts" -type f -exec grep -l "ProtoSubgraph" {} \; | head -10Repository: wundergraph/cosmo
Length of output: 265
🏁 Script executed:
grep -n "ProtoSubgraph\|pluginData" ./controlplane/src/types/index.ts | head -30Repository: wundergraph/cosmo
Length of output: 147
🏁 Script executed:
sed -n '111,120p' ./controlplane/src/types/index.tsRepository: wundergraph/cosmo
Length of output: 218
Restore the old gRPC SubgraphDTO semantics in the mapper.
The new listByFederatedGraph mapper creates proto for every gRPC subgraph regardless of whether schemaVersionId exists, and it never populates proto.pluginData for grpc_plugin. The previous getSubgraph() implementation (used by byTargetId()) only created proto when schemaVersionId was not null and included plugin image metadata in pluginData for grpc_plugin types. This breaks the DTO contract and changes the behavior of callers relying on consistent semantics between the two methods.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@controlplane/src/core/repositories/SubgraphRepository.ts` around lines 985 -
1023, The mapper in listByFederatedGraph is creating a ProtoSubgraph for every
gRPC subgraph even when schemaVersionId is missing and it fails to populate
plugin metadata for grpc_plugin; update the mapping so that proto is only
created when sg.schemaVersionId (or svSchemaSDL equivalent) exists (matching
getSubgraph/byTargetId behavior) and, when sg.type === 'grpc_plugin', populate
proto.pluginData with the plugin image/metadata fields (the same properties
getSubgraph used previously — e.g., plugin image/name/version fields from the sg
record) so the SubgraphDTO/ProtoSubgraph contract is preserved between
listByFederatedGraph and getSubgraph.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@controlplane/test/test-util.ts`:
- Line 119: The fallback default for Keycloak API URL in variable apiUrl was
changed to use port 8090; revert this so that const apiUrl =
process.env.KC_API_URL || 'http://localhost:8080' (restore port 8080) in
controlplane/test/test-util.ts so tests use the same Keycloak port as the test
environment when KC_API_URL is not set; update the literal fallback value only
(leave process.env.KC_API_URL usage intact).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2e01267e-afb0-4c4b-8381-32d9f97b94bd
📒 Files selected for processing (3)
controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.tscontrolplane/src/core/repositories/SubgraphRepository.tscontrolplane/test/test-util.ts
💤 Files with no reviewable changes (1)
- controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- controlplane/src/core/repositories/SubgraphRepository.ts
Summary by CodeRabbit
GetFeatureFlagsInLatestCompositionByFederatedGraphretrieves feature flags in the latest composition;GetFeatureSubgraphsByFederatedGraphretrieves feature subgraphs with pagination and search support.Checklist